package com.admin.system.modules.mnt.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.admin.common.exception.BadRequestException;
import com.admin.system.modules.mnt.dto.DeployQueryDTO;
import com.admin.system.modules.mnt.entity.App;
import com.admin.system.modules.mnt.entity.Deploy;
import com.admin.system.modules.mnt.entity.DeployHistory;
import com.admin.system.modules.mnt.entity.Server;
import com.admin.system.modules.mnt.mapper.DeployMapper;
import com.admin.system.modules.mnt.mapper.DeployServerMapper;
import com.admin.system.modules.mnt.service.DeployHistoryService;
import com.admin.system.modules.mnt.service.DeployService;
import com.admin.system.modules.mnt.service.ServerService;
import com.admin.system.modules.mnt.util.ExecuteShellUtil;
import com.admin.system.modules.mnt.util.ScpClientUtil;
import com.admin.system.modules.mnt.websocket.MsgType;
import com.admin.system.modules.mnt.websocket.SocketMsg;
import com.admin.system.modules.mnt.websocket.WebSocketServer;
import com.admin.oauth.util.LoginUserUtil;
import com.admin.common.utils.PageResult;
import com.admin.common.utils.PageUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Set;

/**
 * @author sq-lan
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DeployServiceImpl extends ServiceImpl<DeployMapper, Deploy> implements DeployService {

    private final String FILE_SEPARATOR = "/";
    private final DeployMapper deployMapper;
    private final DeployServerMapper deployServerMapper;
    private final ServerService serverService;
    private final DeployHistoryService deployHistoryService;
    /**
     * 循环次数
     */
    private final Integer count = 30;

    @Override
    public PageResult<Deploy> paging(DeployQueryDTO dto) {
        PageHelper.startPage(dto.getCurrent(), dto.getSize());
        List<Deploy> list = this.list(dto);
        return PageUtil.toPageHelper(list);
    }

    @Override
    public List<Deploy> list(DeployQueryDTO dto) {
        return deployMapper.list(dto);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void add(Deploy entity) {
        entity.setAppId(entity.getApp().getId());
        save(entity);
        // 保存关联关系
        deployServerMapper.insertData(entity.getId(), entity.getDeploys());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void modify(Deploy entity) {
        Deploy deploy = getById(entity.getId());
        BeanUtil.copyProperties(entity, deploy, CopyOptions.create().setIgnoreNullValue(true));
        saveOrUpdate(deploy);
        // 更新关联关系
        deployServerMapper.deleteByDeployId(entity.getId());
        deployServerMapper.insertData(entity.getId(), entity.getDeploys());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void remove(Set<Integer> ids) {
        removeByIds(ids);
        // 删除关联
        deployServerMapper.deleteByDeployIds(ids);
    }

    @Override
    public void deploy(String fileSavePath, Integer id) {
        deployApp(fileSavePath, id);
    }

    /**
     * @param fileSavePath 本机路径
     * @param id           ID
     */
    private void deployApp(String fileSavePath, Integer id) {
        Deploy deploy = deployMapper.getDeployById(id);
        if (deploy == null) {
            sendMsg("部署信息不存在", MsgType.ERROR);
            throw new BadRequestException("部署信息不存在");
        }
        App app = deploy.getApp();
        if (app == null) {
            sendMsg("包对应应用信息不存在", MsgType.ERROR);
            throw new BadRequestException("包对应应用信息不存在");
        }
        int port = app.getPort();
        //这个是服务器部署路径
        String uploadPath = app.getUploadPath();
        StringBuilder sb = new StringBuilder();
        String msg;
        Set<Server> deploys = deploy.getDeploys();
        for (Server server : deploys) {
            String ip = server.getIp();
            ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip);
            //判断是否第一次部署
            boolean flag = checkFile(executeShellUtil, app);
            //第一步要确认服务器上有这个目录
            executeShellUtil.execute("mkdir -p " + app.getUploadPath());
            executeShellUtil.execute("mkdir -p " + app.getBackupPath());
            executeShellUtil.execute("mkdir -p " + app.getDeployPath());
            //上传文件
            msg = String.format("登陆到服务器:%s", ip);
            ScpClientUtil scpClientUtil = getScpClientUtil(ip);
            log.info(msg);
            sendMsg(msg, MsgType.INFO);
            msg = String.format("上传文件到服务器:%s<br>目录:%s下，请稍等...", ip, uploadPath);
            sendMsg(msg, MsgType.INFO);
            scpClientUtil.putFile(fileSavePath, uploadPath);
            if (flag) {
                sendMsg("停止原来应用", MsgType.INFO);
                //停止应用
                stopApp(port, executeShellUtil);
                sendMsg("备份原来应用", MsgType.INFO);
                //备份应用
                backupApp(executeShellUtil, ip, app.getDeployPath() + FILE_SEPARATOR, app.getName(), app.getBackupPath() + FILE_SEPARATOR, id);
            }
            sendMsg("部署应用", MsgType.INFO);
            //部署文件,并启动应用
            String deployScript = app.getDeployScript();
            executeShellUtil.execute(deployScript);
            sleep(3);
            sendMsg("应用部署中，请耐心等待部署结果，或者稍后手动查看部署状态", MsgType.INFO);
            int i = 0;
            boolean result = false;
            // 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
            while (i++ < count) {
                result = checkIsRunningStatus(port, executeShellUtil);
                if (result) {
                    break;
                }
                // 休眠6秒
                sleep(6);
            }
            sb.append("服务器:").append(server.getName()).append("<br>应用:").append(app.getName());
            sendResultMsg(result, sb);
            executeShellUtil.close();
        }
    }

    private void sleep(int second) {
        try {
            Thread.sleep(second * 1000L);
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
        }
    }

    private void backupApp(ExecuteShellUtil executeShellUtil, String ip, String fileSavePath, String appName, String backupPath, Integer id) {
        String deployDate = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
        StringBuilder sb = new StringBuilder();
        backupPath += appName + FILE_SEPARATOR + deployDate + "\n";
        sb.append("mkdir -p ").append(backupPath);
        sb.append("mv -f ").append(fileSavePath);
        sb.append(appName).append(" ").append(backupPath);
        log.info("备份应用脚本:" + sb.toString());
        executeShellUtil.execute(sb.toString());
        //还原信息入库
        DeployHistory deployHistory = new DeployHistory();
        deployHistory.setAppName(appName);
        deployHistory.setDeployUser(LoginUserUtil.userName());
        deployHistory.setIp(ip);
        deployHistory.setDeployId(id);
        deployHistoryService.add(deployHistory);
    }

    /**
     * 停App
     *
     * @param port             端口
     * @param executeShellUtil /
     */
    private void stopApp(int port, ExecuteShellUtil executeShellUtil) {
        //发送停止命令
        executeShellUtil.execute(String.format("lsof -i :%d|grep -v \"PID\"|awk '{print \"kill -9\",$2}'|sh", port));

    }

    /**
     * 指定端口程序是否在运行
     *
     * @param port             端口
     * @param executeShellUtil /
     * @return true 正在运行  false 已经停止
     */
    private boolean checkIsRunningStatus(int port, ExecuteShellUtil executeShellUtil) {
        String result = executeShellUtil.executeForResult(String.format("fuser -n tcp %d", port));
        return result.indexOf("/tcp:") > 0;
    }

    private void sendMsg(String msg, MsgType msgType) {
        try {
            WebSocketServer.sendInfo(new SocketMsg(msg, msgType), "deploy");
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    @Override
    public String serverStatus(Deploy entity) {
        Set<Server> servers = entity.getDeploys();
        App app = entity.getApp();
        for (Server server : servers) {
            StringBuilder sb = new StringBuilder();
            ExecuteShellUtil executeShellUtil = getExecuteShellUtil(server.getIp());
            sb.append("服务器:").append(server.getName()).append("<br>应用:").append(app.getName());
            boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil);
            if (result) {
                sb.append("<br>正在运行");
                sendMsg(sb.toString(), MsgType.INFO);
            } else {
                sb.append("<br>已停止!");
                sendMsg(sb.toString(), MsgType.ERROR);
            }
            log.info(sb.toString());
            executeShellUtil.close();
        }
        return "执行完毕";
    }

    private boolean checkFile(ExecuteShellUtil executeShellUtil, App app) {
        String result = executeShellUtil.executeForResult("find " + app.getDeployPath() + " -name " + app.getName());
        return result.indexOf(app.getName()) > 0;
    }

    /**
     * 启动服务
     *
     * @param entity /
     * @return /
     */
    @Override
    public String start(Deploy entity) {
        Set<Server> deploys = entity.getDeploys();
        App app = entity.getApp();
        for (Server deploy : deploys) {
            StringBuilder sb = new StringBuilder();
            ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp());
            //为了防止重复启动，这里先停止应用
            stopApp(app.getPort(), executeShellUtil);
            sb.append("服务器:").append(deploy.getName()).append("<br>应用:").append(app.getName());
            sendMsg("下发启动命令", MsgType.INFO);
            executeShellUtil.execute(app.getStartScript());
            sleep(3);
            sendMsg("应用启动中，请耐心等待启动结果，或者稍后手动查看运行状态", MsgType.INFO);
            int i = 0;
            boolean result = false;
            // 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
            while (i++ < count) {
                result = checkIsRunningStatus(app.getPort(), executeShellUtil);
                if (result) {
                    break;
                }
                // 休眠6秒
                sleep(6);
            }
            sendResultMsg(result, sb);
            log.info(sb.toString());
            executeShellUtil.close();
        }
        return "执行完毕";
    }

    /**
     * 停止服务
     *
     * @param entity /
     * @return /
     */
    @Override
    public String stop(Deploy entity) {
        Set<Server> deploys = entity.getDeploys();
        App app = entity.getApp();
        for (Server deploy : deploys) {
            StringBuilder sb = new StringBuilder();
            ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp());
            sb.append("服务器:").append(deploy.getName()).append("<br>应用:").append(app.getName());
            sendMsg("下发停止命令", MsgType.INFO);
            //停止应用
            stopApp(app.getPort(), executeShellUtil);
            sleep(1);
            boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil);
            if (result) {
                sb.append("<br>关闭失败!");
                sendMsg(sb.toString(), MsgType.ERROR);
            } else {
                sb.append("<br>关闭成功!");
                sendMsg(sb.toString(), MsgType.INFO);
            }
            log.info(sb.toString());
            executeShellUtil.close();
        }
        return "执行完毕";
    }

    @Override
    public String reduction(DeployHistory entity) {
        Integer deployId = entity.getDeployId();
        Deploy deployInfo = getById(deployId);
        String deployDate = DateUtil.format(entity.getDeployDate(), DatePattern.PURE_DATETIME_PATTERN);
        App app = deployInfo.getApp();
        if (app == null) {
            sendMsg("应用信息不存在：" + entity.getAppName(), MsgType.ERROR);
            throw new BadRequestException("应用信息不存在：" + entity.getAppName());
        }
        String backupPath = app.getBackupPath() + FILE_SEPARATOR;
        backupPath += entity.getAppName() + FILE_SEPARATOR + deployDate;
        //这个是服务器部署路径
        String deployPath = app.getDeployPath();
        String ip = entity.getIp();
        ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip);
        String msg;

        msg = String.format("登陆到服务器:%s", ip);
        log.info(msg);
        sendMsg(msg, MsgType.INFO);
        sendMsg("停止原来应用", MsgType.INFO);
        //停止应用
        stopApp(app.getPort(), executeShellUtil);
        //删除原来应用
        sendMsg("删除应用", MsgType.INFO);
        executeShellUtil.execute("rm -rf " + deployPath + FILE_SEPARATOR + entity.getAppName());
        //还原应用
        sendMsg("还原应用", MsgType.INFO);
        executeShellUtil.execute("cp -r " + backupPath + "/. " + deployPath);
        sendMsg("启动应用", MsgType.INFO);
        executeShellUtil.execute(app.getStartScript());
        sendMsg("应用启动中，请耐心等待启动结果，或者稍后手动查看启动状态", MsgType.INFO);
        int i = 0;
        boolean result = false;
        // 由于启动应用需要时间，所以需要循环获取状态，如果超过30次，则认为是启动失败
        while (i++ < count) {
            result = checkIsRunningStatus(app.getPort(), executeShellUtil);
            if (result) {
                break;
            }
            // 休眠6秒
            sleep(6);
        }
        StringBuilder sb = new StringBuilder();
        sb.append("服务器:").append(ip).append("<br>应用:").append(entity.getAppName());
        sendResultMsg(result, sb);
        executeShellUtil.close();
        return "";
    }

    private ExecuteShellUtil getExecuteShellUtil(String ip) {
        Server server = serverService.getByIp(ip);
        if (server == null) {
            sendMsg("IP对应服务器信息不存在：" + ip, MsgType.ERROR);
            throw new BadRequestException("IP对应服务器信息不存在：" + ip);
        }
        return new ExecuteShellUtil(ip, server.getAccount(), server.getPassword(), server.getPort());
    }

    private ScpClientUtil getScpClientUtil(String ip) {
        Server server = serverService.getByIp(ip);
        if (server == null) {
            sendMsg("IP对应服务器信息不存在：" + ip, MsgType.ERROR);
            throw new BadRequestException("IP对应服务器信息不存在：" + ip);
        }
        return ScpClientUtil.getInstance(ip, server.getPort(), server.getAccount(), server.getPassword());
    }

    private void sendResultMsg(boolean result, StringBuilder sb) {
        if (result) {
            sb.append("<br>启动成功!");
            sendMsg(sb.toString(), MsgType.INFO);
        } else {
            sb.append("<br>启动失败!");
            sendMsg(sb.toString(), MsgType.ERROR);
        }
    }

}
